package com.xxwu.messagequeue.mqserver.datacenter;


import com.xxwu.messagequeue.common.BinaryTool;
import com.xxwu.messagequeue.common.MqException;
import com.xxwu.messagequeue.mqserver.core.MSGQueue;
import com.xxwu.messagequeue.mqserver.core.Message;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

@Slf4j
public class MessageFileManager {
    static public class Stat{
        public int totalCount = 0;
        public int validCount = 0;
    }

    // 先写三个方法用于获取消息目录和文件
    private String getBasicPath(String queueName){
        return "./data/" + queueName;
    }
    private String getMessageDataPath(String queueName){
        return "./data/" + queueName + "/queue_data.txt";
    }
    private String getMessageStatPath(String queueName){
        return "./data/" + queueName + "/queue_stat.txt";
    }

    public void init(){
        // 该方法是为了实现与数据库操作代码统一，此处不做任何操作
    }
    // 从消息统计文件读数据
    private Stat readStat(String queueName){
        Stat stat = new Stat();
        try(InputStream is = new FileInputStream(getMessageStatPath(queueName))){
            Scanner sc = new Scanner(is);
            stat.totalCount = sc.nextInt();
            stat.validCount = sc.nextInt();
        }catch (IOException e){
            e.printStackTrace();
        }
        return stat;
    }

    // 向消息统计文件写数据
    private void writeStat(String queueName, Stat stat){
        // 使用 OutputStream 打开文件进行写入，默认会把原文件的内容覆盖
        try(OutputStream os = new FileOutputStream(getMessageStatPath(queueName))){
            PrintWriter ps = new PrintWriter(os);
            ps.println(stat.totalCount);
            ps.println(stat.validCount);
            ps.flush();
        } catch (IOException e){
            e.printStackTrace();
        }
    }
    // 创建队列对应目录以及相应文件
    public void createQueueFile(String queueName) throws IOException, MqException {
        File messageDir = new File(getBasicPath(queueName));
        if(!messageDir.exists()){
            boolean result = messageDir.mkdirs();
            if(!result) {
                throw new MqException("[createQueueFile],创建主目录失败");
            }
        }

        File messageDataFile = new File(getMessageDataPath(queueName));
        if(!messageDataFile.exists()){
            boolean result = messageDataFile.createNewFile();
            if(!result){
                throw new MqException("[createQueueFile],创建 data 文件失败失败");
            }
        }

        File messageStatFile = new File(getMessageStatPath(queueName));
        if(!messageStatFile.exists()){
            boolean result = messageStatFile.createNewFile();
            if(!result){
                throw new MqException("[createQueueFile],创建 stat 文件失败失败");
            }
        }
        Stat stat = new Stat();
        // 设定统计消息文件的初始值0
        writeStat(queueName, stat);
    }

    // 删除队列的目录和文件， 要先删除两个子文件才能删除目录
    public boolean destroyQueueFile(String queueName){
        File messageDataFile = new File(getMessageDataPath(queueName));
        boolean result1 = messageDataFile.delete();
        File messageStatFile = new File(getMessageStatPath(queueName));
        boolean result2 = messageStatFile.delete();
        File messageDir = new File(getBasicPath(queueName));
        boolean result3 = messageDir.delete();
        if(result1 && result2 && result3){
            log.info("deleteDir 删除目录成功！");
            return true;
        }
        return false;
    }

    // 判断队列文件是否存在，若不存在则不能进行后续工作
    public boolean checkFileExist(String queueName){
        File messageDataFile = new File(getMessageDataPath(queueName));
        if(!messageDataFile.exists()){
            return false;
        }
        File messageStatFile = new File(getMessageStatPath(queueName));
        if(!messageStatFile.exists()){
            return false;
        }
        return true;
    }

    // 将消息写入消息数据文件
    // 在消息文件的写入和删除时，涉及到要对 queue 对象进行加锁，所以这里需要传入 queue
    // 其他不涉及到加锁的场景直接传入队列名称即可
    public void insertMessage(MSGQueue queue, Message message) throws MqException, IOException {
        // 1 检查消息要插入的文件是否存在
        File messageDataFile = new File(getMessageDataPath(queue.getName()));
        if(!messageDataFile.exists()){
            throw new MqException("[MessageFileManager]，要插入的队列数据文件不存在");
        }
        // 2 将消息进行序列化
        byte[] disMessage = BinaryTool.tobytes(message);
        synchronized (queue){
            // 3 设置 message 在文件中的位置，在后续删除操作中会用到
            // 虽然这两个属性不会存入到硬盘中，但后续拿着 message 对象进行删除操作时要清楚该对象存在硬盘中的哪个位置才能删除
            message.setOffsetBeg(messageDataFile.length() + 4);
            message.setOffsetEnd(messageDataFile.length() + 4 + disMessage.length);
            // 4 将消息插入队列， 这里是追加写，不能因打开 OutputStream 导致之前的消息全部清空
            try(OutputStream os = new FileOutputStream(messageDataFile, true)){
                // 这里的消息长度是四个字节，使用 OutputStream 的 write 方法无法一次性写入四个字
                // 使用 DataOutputStream 可以实现一次写四个字节的操作
                try(DataOutputStream dos = new DataOutputStream(os)){
                    dos.writeInt(disMessage.length);
                    dos.write(disMessage);
                }
            }
            //更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount++;
            stat.validCount++;
            writeStat(queue.getName(), stat);
        }
    }

    // 将消息从数据文件中删除
    public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException {
        synchronized (queue) {
            // 这里的读操作是根据下标随机访问的，所以不能用 FileInputStream
            // rw 的意思是可读可写
            try (RandomAccessFile raf = new RandomAccessFile(getMessageDataPath(queue.getName()), "rw")) {
                // 1 先从消息文件中读取文件
                byte[] disMessage = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                // 移动光标到消息头
                raf.seek(message.getOffsetBeg());
                // 读取变长数组大小的文件
                raf.read(disMessage);
                // 2 将字节数组转化为 Message 对象，反序列化
                Message message1 = (Message) BinaryTool.frombytes(disMessage);
                // 3 修改对象的 isValid 属性
                message1.setIsValid((byte) 0x0);
                // 4 将对象序列化
                byte[] result = BinaryTool.tobytes(message1);
                // 5 将对象写回消息文件中
                // 注意，这里的 seek 光标会随着读写操作移动,要将光标移回原来消息头的位置
                raf.seek(message.getOffsetBeg());
                raf.write(result);
            }
            // 更新消息统计文件
            Stat stat = readStat(queue.getName());
            if(stat.validCount > 0){
                stat.validCount--;
            }
            writeStat(queue.getName(), stat);
        }
    }

    // 将消息从硬盘中读到内存
    // 这个操作是程序加载时就进行的，此时程序还无法进行任何业务逻辑的处理，不用进行加锁
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws MqException, IOException, ClassNotFoundException {
        // 1 获取消息文件路径
        File messageDataFile = new File(getMessageDataPath(queueName));
        if(!messageDataFile.exists()){
            throw new MqException("[MessageFileManager]，要读取的文件不存在");
        }
        LinkedList<Message> messages = new LinkedList<>();
        int location = 0;
        try(InputStream inputStream = new FileInputStream(messageDataFile)){
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)){
                while(true){
                    // 当读到文件末尾时，raedInt 会抛出异常，这个异常不属于要处理的范围，直接声明读取成功即可
                    // 这里就是循环的出口
                    int messageSize = dataInputStream.readInt();
//                    System.out.println("messageSize:"+messageSize);
                    byte[] buffer = new byte[messageSize];
                    // 2 读取文件
                    int actualLength = dataInputStream.read(buffer);
                    if(buffer.length != actualLength){
                        throw new MqException("[MessageFileManager]，读取的文件异常，queueName=" + messageDataFile.getAbsoluteFile());
                    }
                    // 3 将文件转化为 message 对象
                    Message message = (Message) BinaryTool.frombytes(buffer);
                    // 判断文件是否有效
                    if(message.getIsValid() != 0x1){
                        // 读取的文件无效，直接跳过，并且计数器也要更新
                        location += (4 + messageSize);
//                        return messages;
                        continue;
                    }
                    // 4 设置文件在硬盘中的存储位置
                    // 由于 DataInputStream 无法直接获取到当前光标的位置，所以这里手动计算
                    message.setOffsetBeg(location + 4);
                    message.setOffsetEnd(location + 4 + messageSize);
                    // 5 更新计数器
                    location += (4 + messageSize);
                    // 6 将文件添加在链表中
                    messages.add(message);
                }
            }catch (EOFException e){
                log.info("文件读取完成！{}",messageDataFile.getAbsolutePath());
            }
        }
        return messages;
    }

    // 判断是否需要触发gc
    public boolean checkGC(String queueName){
        // 判断消息统计文件的数目是否达到触发GC的标准
        Stat stat = readStat(queueName);
        // 要注意这里的 stat.validCount 和 stat.totalCount 都是 int 最后得到的结果一定为0
        // 所以要进行类型转换
        if(stat.totalCount > 2000 && (double) stat.validCount / (double) stat.totalCount > 0.5){
            return true;
        }
        return false;
    }

    // 创建新消息文件路径
    public String getNewMessageDataPath(String queueName){
        return "./data/" + queueName + "/queue_newData.txt";
    }

    // 实现消息的垃圾回收，这里使用复制算法
    // 将消息从文件中读取，写入新文件中，再将新文件重命名回旧文件
    // 在消息文件进行垃圾回收时，其他线程不可进行读取
    public void gc(MSGQueue queue) throws IOException, MqException, ClassNotFoundException {
        synchronized (queue){
            long timeBegin = System.currentTimeMillis();
            File newFile = new File(getNewMessageDataPath(queue.getName()));
            if(newFile.exists()){
                throw new MqException("[MessageFileManager]，上次GC未完成，文件名为：" + newFile.getAbsoluteFile());
            }
            // 1 创建新文件
            boolean ok = newFile.createNewFile();
            if(!ok){
                throw new MqException("[MessageFileManager]，文件创建失败，文件名为：" + newFile.getAbsoluteFile());
            }
            // 2 读取旧文件的消息
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());
            // 3 遍历旧文件，将旧文件的数据写入到新文件
            // 虽然有一个写入文件的方法，但方法内部实现的是写入旧文件，要使用就要更改该方法，所以这里重新写
            try(OutputStream outputStream = new FileOutputStream(newFile)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    for(Message message : messages){
                        // 将旧文件序列化
                        byte[] buffer = BinaryTool.tobytes(message);
                        // 先写入长度
                        dataOutputStream.writeInt(buffer.length);
                        // 再写入内容
                        dataOutputStream.write(buffer);
                    }
                }
            }
            // 4 删除旧文件
            File oldFile = new File(getMessageDataPath(queue.getName()));
            ok = oldFile.delete();
            if(!ok){
                throw new MqException("旧文件删除失败！" + oldFile.getAbsoluteFile());
            }
            // 5 将新文件重命名为旧文件
            ok = newFile.renameTo(oldFile);
            if(!ok){
                throw new MqException("新文件重命失败！" + newFile.getAbsoluteFile());
            }
            // 6 更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queue.getName(), stat);
            // 输出GC时间
            long timeEnd = System.currentTimeMillis();
            log.info("GC完成的时间：{}",timeEnd - timeBegin);
        }
    }
}
